#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2017 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent
from packagelib import *
from testlib import *


@skipImage("Not supported", "fedora-coreos")
@skipPackage("cockpit-apps")
class TestApps(PackageCase):

    def setUp(self):
        super().setUp()
        self.appstream_collection = set()
        self.machine.upload(["verify/files/test.png"], "/var/tmp/")

    def createAppStreamPackage(self, name, version, revision):
        self.createPackage(name, version, revision, content={
            "/usr/share/metainfo/org.cockpit-project.{0}.metainfo.xml".format(name): """
<component type="addon">
  <extends>org.cockpit_project.cockpit</extends>
  <id>org.cockpit-project.{0}</id>
  <icon type="local">/usr/share/pixmaps/test.png</icon>
  <name>{0}</name>
  <summary>An application for testing</summary>
  <launchable type="cockpit-manifest">{0}</launchable>
</component>
""".format(name),
            "/usr/share/pixmaps/test.png": {"path": "/var/tmp/test.png"}})
        self.appstream_collection.add(name)

    def createAppStreamRepoPackage(self):
        body = ""
        for p in self.appstream_collection:
            body += """
  <component type="addon">
  <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.{0}</id>
    <icon type="cached">test.png</icon>
    <name>{0}</name>
    <summary>An application for testing</summary>
    <launchable type="cockpit-manifest">{0}</launchable>
    <description>
        <p>DESCRIPTION:none</p>
    </description>
    <url type="homepage">https://{0}.com</url>
    <pkgname>{0}</pkgname>
  </component>
""".format(p)
        self.machine.execute("mkdir -p /usr/share/app-info/xmls")
        self.createPackage("appstream-data-test", "1.0", "1", content={
            "/usr/share/app-info/xmls/test.xml": """
<components origin="test">
{0}
</components>
            """.format(body),
            "/usr/share/app-info/icons/test/64x64/test.png": {"path": "/var/tmp/test.png"}})
        self.enableRepo()
        self.machine.execute("systemctl stop packagekit && pkcon refresh force")
        # ignore the corresponding journal entry
        self.allow_journal_messages("org.freedesktop.PackageKit.*org.freedesktop.DBus.Error.NoReply.*")

    def testBasic(self, urlroot=""):
        b = self.browser
        m = self.machine

        self.allow_journal_messages("can't remove watch: Invalid argument")

        # Make sure none of the appstream directories exist.  They
        # will be created later and we need to cope with that.

        m.execute("rm -rf /usr/share/metainfo /usr/share/app-info /var/cache/app-info")

        # instead of the actual distro packages, use our own fake repo data package
        m.write("/usr/share/cockpit/apps/override.json",
                '{ "config": { "appstream_data_packages": [ "appstream-data-test" ] } }')

        if urlroot != "":
            m.execute('mkdir -p /etc/cockpit/ && echo "[WebService]\nUrlRoot=%s" > /etc/cockpit/cockpit.conf' % urlroot)

        m.start_cockpit()
        b.login_and_go("/apps", urlroot=urlroot)
        b.wait_visible(".app-list-empty")

        self.createAppStreamPackage("app-1", "1.0", "1")
        self.createAppStreamRepoPackage()

        # Refresh package info
        b.click("#refresh")

        b.click(".app-list #app-1")
        b.wait_visible('a[href="https://app-1.com"]')
        b.wait_visible('#app-page img[src^="%s/cockpit/channel/"]' % urlroot)
        b.click(".pf-c-breadcrumb a:contains('Applications')")

        b.wait_visible("#list-page")
        b.wait_not_visible("#app-page")

        b.click(".app-list .pf-c-data-list__item-row:contains('app-1') button:contains('Install')")
        b.wait_visible(".app-list .pf-c-data-list__item-row:contains('app-1') button:contains('Remove')")
        b.wait_visible(".app-list .pf-c-data-list__item-row:contains('app-1') img[src^='%s/cockpit/channel/']" % urlroot)
        m.execute("test -f /stamp-app-1-1.0-1")

        b.click(".app-list .pf-c-data-list__item-row:contains('app-1') button:contains('Remove')")
        b.wait_visible(".app-list .pf-c-data-list__item-row:contains('app-1') button:contains('Install')")
        b.wait_visible(".app-list .pf-c-data-list__item-row:contains('app-1') img[src^='%s/cockpit/channel/']" % urlroot)
        m.execute("! test -f /stamp-app-1-1.0-1")

    def testWithUrlRoot(self):
        self.testBasic(urlroot="/webcon")

    def testL10N(self):
        b = self.browser
        m = self.machine

        # Switching to a language might produce these messages, which seem to be harmless.
        self.allow_journal_messages("invalid or unusable locale.*",
                                    "Error .* data: Connection reset by peer")

        # Because of the reloading done by set_lang
        self.allow_restart_journal_messages()

        # Reset everything
        m.execute("rm -rf /usr/share/metainfo /usr/share/app-info /var/cache/app-info")

        m.start_cockpit()
        b.login_and_go("/apps")
        b.wait_visible(".app-list-empty")

        m.execute("mkdir -p /usr/share/app-info/xmls")
        m.write("/usr/share/app-info/xmls/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.foo</id>
    <name>NAME:none</name>
    <name xml:lang="de">NAME:de</name>
    <summary>SUMMARY:none</summary>
    <summary xml:lang="de">SUMMARY:de</summary>
    <description>
        <p>DESCRIPTION:none</p>
        <p xml:lang="de">DESCRIPTION:de</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")

        b.wait_visible(".app-list .pf-c-data-list__item-row:contains('SUMMARY:none') button:contains('Install')")
        b.click(".app-list .pf-c-data-list__item-row:contains('SUMMARY:none') .pf-m-inline")

        b.wait_visible(".app-description:contains('DESCRIPTION:none')")

        def set_lang(lang):
            b.switch_to_top()
            b.click("#navbar-dropdown")
            b.click(".display-language-menu a")
            b.wait_visible('#display-language-modal')
            if self.system_before(242):  # Changed in #15667
                b.set_val("#display-language-modal select", lang)
            else:
                b.click('#display-language-modal li[data-value=%s] button' % lang)
            b.click("#display-language-modal footer button.pf-m-primary")
            b.expect_load()
            # HACK: work around language switching in Chrome not working in current session (issue #8160)
            b.reload(ignore_cache=True)
            b.wait_visible("#content")
            b.enter_page("/apps")

        set_lang("de-de")
        b.wait_visible(".app-description:contains('DESCRIPTION:de')")
        b.wait_not_present(".app-description:contains('DESCRIPTION:none')")

        set_lang("ja-jp")
        b.wait_visible(".app-description:contains('DESCRIPTION:none')")
        b.wait_not_present(".app-description:contains('DESCRIPTION:de')")

        # like in the general whitelist, but translated
        self.allow_journal_messages("xargs: basename: .*Signal 13.*")

    def testBrokenXML(self):
        b = self.browser
        m = self.machine

        # Reset everything
        m.execute("rm -rf /usr/share/metainfo /usr/share/app-info /var/cache/app-info")

        m.start_cockpit()
        b.login_and_go("/apps")
        b.wait_visible(".app-list-empty")

        m.execute("mkdir -p /usr/share/app-info/xmls")

        self.allow_journal_messages(".*/usr/share/app-info/xmls/test.xml.*",
                                    ".*xml.etree.ElementTree.ParseError.*")

        def reset():
            m.write("/usr/share/app-info/xmls/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
            b.wait_not_present(".app-list-empty")
            b.wait_visible(".app-list .pf-c-data-list__item-row:contains('Summary')")

        # First lay down some good XML so that we can later detect the reaction to broken XML.
        reset()

        # Unparsable
        m.write("/usr/share/app-info/xmls/test.xml", """
This <is <not XML.
""")
        b.wait_visible(".app-list-empty")
        reset()

        # Not really AppStream
        m.write("/usr/share/app-info/xmls/test.xml", """
<foo></foo>
""")
        b.wait_visible(".app-list-empty")
        reset()

        # No origin
        m.write("/usr/share/app-info/xmls/test.xml", """
<components>
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
        b.wait_visible(".app-list-empty")
        reset()

        # No package
        m.write("/usr/share/app-info/xmls/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
  </component>
</components>""")
        b.wait_visible(".app-list-empty")
        reset()

        # No id
        m.write("/usr/share/app-info/xmls/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <name>Name</name>
    <summary>No description</summary>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
        b.wait_visible(".app-list-empty")
        reset()

        # Error (launchable without type) in earlier entry, shouldn't affect the later entry
        m.write("/usr/share/app-info/xmls/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test2</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description 2</p>
    </description>
    <launchable>foo</launchable>
    <pkgname>foo</pkgname>
  </component>
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary 2</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
        b.wait_not_present(".app-list-empty")
        b.wait_visible(".app-list .pf-c-data-list__item-row:contains('Summary 2')")


if __name__ == '__main__':
    test_main()
